package gretry

import (
	"context"
	"time"
)

// DefaultDelayCalculator 指数退避+抖动.
// 第一次尝试等待 200ms + random(50 - 150)ms,
// 第二次尝试等待 200ms + random(50 - 150)ms,
// 第三次尝试等待 400ms + random(50 - 150)ms, ...
// 最大延迟不超过 2.15s.
var DefaultDelayCalculator = DCWithJitter(
	DCExponential(200*time.Millisecond, 2*time.Second),
	50*time.Millisecond, 150*time.Millisecond,
)

const (
	DefaultAttempts = 6
)

type RetryIf func(error) bool

type TimerFun func(time.Duration) <-chan time.Time

type retester[T any] struct {
	attempts        uint            // 最大重试次数
	delayCalculator DelayCalculator // 延迟计算器
	retryIf         RetryIf         // 是否进行重试
	timer           TimerFun        // 延迟函数
}

func (r *retester[T]) setAttempts(u uint) {
	r.attempts = u
}

func (r *retester[T]) setDelayCalculator(dc DelayCalculator) {
	r.delayCalculator = dc
}

func (r *retester[T]) setRetryIf(f RetryIf) {
	r.retryIf = f
}

func (r *retester[T]) setTimer(f TimerFun) {
	r.timer = f
}

func newRetester[T any](options ...Option) *retester[T] {
	r := &retester[T]{
		attempts:        DefaultAttempts,
		delayCalculator: DefaultDelayCalculator,
		retryIf:         func(_ error) bool { return true },
		timer:           time.After,
	}
	for _, opt := range options {
		opt.apply(r)
	}
	return r
}

func (r *retester[T]) doWithValue(ctx context.Context, f RetryableWithValueFun[T]) (T, error) {
	var tried uint
	untilSuccess := r.attempts == 0
	for {
		t, err := f(ctx)
		if err == nil {
			return t, nil
		}
		if unRecoverableErr, ok := err.(*unrecoverableError); ok {
			return t, unRecoverableErr.err
		}
		if !untilSuccess {
			if r.attempts == tried {
				return t, err
			}
		}
		if !r.retryIf(err) {
			return t, err
		}
		duration := r.delayCalculator(tried)
		select {
		case <-r.timer(duration):
		case <-ctx.Done():
			return t, err // 上下文被取消时, 返回最后一次遇到的错误
		}
		tried++
	}
}

func DoWithValue[T any](ctx context.Context, f RetryableWithValueFun[T], options ...Option) (T, error) {
	return newRetester[T](options...).doWithValue(ctx, f)
}

type empty struct{}

func Do(ctx context.Context, f RetryableFun, options ...Option) error {
	_, err := newRetester[empty](options...).doWithValue(ctx, func(ctx context.Context) (empty, error) {
		return empty{}, f(ctx)
	})
	return err
}
